/*=============================================================================
    jibits/grid.js
    Copyright (C) 2008-2009, Donald W. Griffin
    All rights reserved.
    [MIT license :: see license.txt for details]

    Portions from:

      SortTable
      version 2
      7th April 2007
      Stuart Langridge, http://www.kryogenix.org/code/browser/sorttable/
      Licenced as X11: http://www.kryogenix.org/code/browser/licence.html
      This basically means: do what you want with it.
      ---
      Dragtable v1.0
      Dan Vanderkam, http://danvk.org/dragtable/
                     http://code.google.com/p/dragtable/
      Licensed under the MIT license.
      ---
      Mike Hall's draggable class (http://www.brainjar.com/dhtml/drag/)
      Do not remove this notice.
      Copyright 2001 by Mike Hall.
      See http://www.brainjar.com for terms of use.

=============================================================================*/
$module("jibits.grid");

$requires("zjs.browser");

$requires("jibits.widget");
$requires("jibits.template");

$namespace(jibits, {

DataBinding : $class(
{
    ctor : function ()
    {
        
    }
}),

DataProvider : $class(
{
    ctor : function (data)
    {
        this.data = data || {};
    }
}),

SortSet : $class(
{
    ctor : function (limit)
    {
        this.limit = limit;
        this.members = [];
        this.index = {};
    },

    add : function (v)
    {
        var m = 1;
        if (v in this.index)
        {
            var k = this.index[v];
            if (Math.abs(k) == this.members.length)
            {
                this.index[v] *= -1;
                return this;
            }

            this.remove(v);
            m = (k < 0) ? -1 : 1;
        }
        else if (this.members.length == this.limit)
            this.remove(this.members[0]);

        this.members.push(v);
        this.index[v] = this.members.length * m;
        return this;
    },

    getClasses : function (v)
    {
        var k = ((v in this.index) ? this.index[v] : 0), a = (k > 0);
        k = Math.abs(k) - 1;

        var c = [ (k < 0 ? "-" : "+") + "sortCol",
                  (k >= 0 && a ? "+" : "-") + "sortAscend",
                  (k < 0 || a ? "-" : "+") + "sortDescend"];

        for (var i = 0, n = this.members.length; i < n; ++i)
            c.push(((i == k) ? "+" : "-") + "sortCol" + (n - i));

        return c;
    },

    has : function (v)
    {
        return v in this.index;
    },

    indexOf : function (v)
    {
        if (v in this.index)
            return Math.abs(this.index[v]) - 1;
        return -1;
    },

    remove : function (v)
    {
        if (v in this.index)
        {
            var k = Math.abs(this.index[v]);
            this.members.splice(--k, 1);
            delete this.index[v];

            while (k++ < this.members.length)
            {
                var m = this.members[k-1], j = this.index[m];
                this.index[m] = (j < 0) ? -k : k;
            }
        }

        return this;
    }
}),

/**

*/
Grid : $class(jibits.Widget, function ()
{
    /*
    Bug in IE: Rows that are not in the document's DOM do not have cells or rows
    properties. See:

        http://jehiah.cz/archive/rows-and-cells-in-ie-when-using-clonenode

    Our hack is to have a hidden table in which we place all our row templates.
    This gets us around the problems and still keeps our row templates out of
    sight.

    This var is the TBODY of our hidden TABLE used to contain all row templates.
    */
    var ieTableHack;

    var MINCOLWIDTH = 10;
    var SPLITSFX = "_splitdiv";

    function createRow (data, index)
    {
        var rowid = this.id + "_r" + index;
        var rd = jibits.drillDown(data[index], data);
        rd.$index = index;

        var ret = this.rowTemplate.generate(rowid, rd);

        return ret;
    }

    function duplicateColumn (part, column, dragObj, opt) // static
    {
        if (opt.rows < 1)
            return;

        var part2 = jibits.cloneElemShallow(part, opt.nofix), width = 0;

        $foreach(part.rows, function (row)
        {
            var cell = row.cells[column];
            var tr = jibits.cloneElemShallow(row, opt.nofix);
            var td = jibits.cloneElemDeep(cell, opt.nofix);

            if (row.offsetHeight)
                tr.style.height = row.offsetHeight + "px";
            if (cell.offsetWidth)
            {
                td.style.width = cell.offsetWidth + "px";
                if (opt.width < cell.offsetWidth)
                    opt.width = cell.offsetWidth;
            }
            tr.appendChild(td);
            part2.appendChild(tr);

            if (opt.opacity < 1)
                jibits.setOpacity(tr, Math.max(0.05, opt.opacity));
            opt.opacity -= opt.fade;

            if (--opt.rows == 0)
                $break();
        });

        dragObj.appendChild(part2);
    }

    function fixHead (tbl)
    {
        // logic from sorttable...

        if (tbl.getElementsByTagName("thead").length == 0)
        {
            // table doesn't have a tHead. Since it should have, create one
            // and put the first table row in it.
            var th = tbl.ownerDocument.createElement("thead");
            th.appendChild(tbl.rows[0]);
            tbl.insertBefore(th, tbl.firstChild);
        }

        // Safari doesn't support table.tHead, sigh
        if (tbl.tHead == null)
            tbl.tHead = tbl.getElementsByTagName('thead')[0];

        $assert(tbl.tHead.rows.length == 1);

        if (this.dragCols || this.sortCols)
        {
            var ticket = this.getTicket(), dragCfg = { handlers:this }, self = this;
            var evmgr = jibits.eventMgr(), onclick = this.onClick.bind(this);
            var dstyle = { position:"relative", width:"100%", height:"100%"};
            var sstyle = { position:"absolute", left:"0px", top:"0px", height:"100%",
                           width:"6px", cursor:"e-resize" };

            $foreach(tbl.tHead.rows[0].cells, function (th, prevRet, index)
            {
                var id = th.id;
                if (!id)
                {
                    var s = jibits.getText(th);
                    th.id = id = tbl.id + "_h" + (zjs.isEmpty(s) ? index : s);
                }
                self.colIndex[id] = index;
                self.colIds[index] = id;

                /*
                We inject a "middle-manager" div as the parent of the contents
                of the TH and the (only) child of the TH. This is because we
                cannot use a TH as a positioned element for positioning child
                elements.
                */
                var div = jibits.newDiv(id+"_posdiv");
                for (var c = th.firstChild, next; c; c = next)
                {
                    next = c.nextSibling;
                    if (c.nodeType == 2) // if (ATTRIBUTE)
                        continue;

                    th.removeChild(c);
                    div.appendChild(c);
                }
                jibits.setStyle(div, dstyle);

                // Add a left-aligned splitter div:
                if (self.resizeCols)
                {
                    var splitter = jibits.newChildDiv(div, id+SPLITSFX);
                    jibits.setStyle(splitter, sstyle);
                    div.appendChild(splitter);
                    if (index == 0)
                        jibits.hide(splitter);
                }

                th.appendChild(div);

                if (self.dragCols)
                {
                    var mgr = new jibits.DragManager(id, dragCfg);
                    mgr.column = id;
                    ticket.add(mgr);
                }
                if (self.resizeCols)
                    ticket.add(getResizer(self, id));
                if (self.sortCols)
                    ticket.add(evmgr.listen(th, "click", onclick.head(id)));
            });
        }
    }

    function getDragColIndex (drag)
    {
        var tbl = this.elem();
        return $foreach(tbl.tHead.rows[0].cells, function (th, prevRet, index)
        {
            if (th.id == drag.mgr.column)
                $break(index);
            return -1;
        });
    }

    function getResizer (self, colId)
    {
        var cfg =
        {
            handlers :
            {
                onDragBegin : function (drag) { return self.onResizeBegin(drag); },
                onDragMove  : function (drag) { return self.onResizeMove(drag); },
                onDragEnd   : function (drag) { return self.onResizeEnd(drag); }
            }
        };

        var mgr = new jibits.DragManager(colId+SPLITSFX, cfg);
        mgr.column = colId;

        return mgr;
    }

    function putRows (rows, tuple)
    {
        var t = this.elem(), tb = t.tBodies[0], tbc = jibits.cloneElemShallow(tb);
        var tuples = typeof(tuple) != "undefined", mod = this.altRowStyles.length;

        for (var i = 0, n = rows.length; i < n; ++i)
        {
            var r = tuples ? rows[i][tuple] : rows[i];
            tbc.appendChild(r);

            if (!mod)
                continue;

            var k = i % mod, s = this.altRowStyles[k];
            this.altRowStyles[k] = this.altRowStylesAdd[k];
            jibits.modifyClasses(r, this.altRowStyles);
            this.altRowStyles[k] = s;
        }

        tb.parentNode.replaceChild(tbc, tb);
    }

    // Based on the "Swapping table columns" discussion on comp.lang.javascript.
    // Copied from dragtable
    function moveColumn (row, from, to) // static
    {
        var x = row.removeChild(row.cells[from]);
        if (to < row.cells.length)
            row.insertBefore(x, row.cells[to]);
        else
            row.appendChild(x);
    }

    function whichColumn (x)
    {
        var tbl = this.elem();
        return $foreach(tbl.tHead.rows[0].cells, function (th, prevRet, index)
        {
            var r = jibits.getTotalOffsetRect(th);
            if (r.horz.contains(x))
            {
                var ret = { x : r.X(), y : r.Y(), col : index };

                if (x > r.horz.middle())
                {
                    ++ret.col;
                    ret.x += r.W();
                }

                $break(ret);
            }
            return null;
        });
    }

    return (
    {
        ctor : function (id, config)
        {
            this.colIndex = {}; // column ID map to column index
            this.colIds = []; // column index map to column ID
            this.dragCols = config.dragColumns;
            this.dragInsertTracker = config.dragInsertTracker;
            this.dragObjRows = config.dragObjRows || (1 << 30); // all by default
            this.dragObjFade = config.dragObjFade || 0.07;
            this.fixedHeaders = config.fixedHeaders;
            this.fixedLeftCol = config.fixedLeftCol;
            this.prov = config.prov;
            this.resizeCols = config.resizeColumns;
            this.rowDesc = config.rowDesc;
            this.scrollBox = config.scrollBox;

            if (!config.altRowStyles)
                this.altRowStyles = ["-row1of2", "-row2of2"];
            else
            {
                this.altRowStyles = [];
                for (var i = 0, n = config.altRowStyles.length; i < n; ++i)
                    this.altRowStyles[i] = "-" + config.altRowStyles[i];
            }

            this.altRowStylesAdd = [];
            for (var k = this.altRowStyles.length; k-- > 0; )
                this.altRowStylesAdd[k] = "+" + this.altRowStyles[k].substring(1);

            if (config.sortColumns)
                this.sortCols = new jibits.SortSet(config.sortColumns);

            $super(arguments).apply(this, arguments); // may call onConnect
        },

        lockHeaders : function (lock)
        {
            this.fixedHeaders = (arguments.length == 0) ? true : !!lock;
            // nothing more we can do here...
        },

        lockLeftColumn : function (lock)
        {
            this.fixedLeftCol = (arguments.length == 0) ? true : !!lock;
            // nothing more we can do here...
        },

        onConnect : function ()
        {
            var tbl = this.elem();

            fixHead.call(this, tbl);

            var rowTemplate = tbl.tBodies[0].rows[0];
            this.rowTemplate = new jibits.Template(rowTemplate, this.rowDesc);
            rowTemplate.parentNode.removeChild(rowTemplate);

            if (zjs.browser.ie)
            {
                if (!ieTableHack)
                {
                    var hack = document.createElement("table");
                    hack.appendChild(ieTableHack = document.createElement("tbody"));
                    jibits.hide(hack);
                    document.body.appendChild(hack);
                }

                ieTableHack.appendChild(rowTemplate); // fix rows/cells in IE
            }

            this.onDataChange();
        },

        onClick : function (colid, ev)
        {
            this.sortCols.add(colid);

            var tbl = $elem(this.id);
            var self = this;
            $foreach(tbl.tHead.rows[0].cells, function (th)
            {
                var c = self.sortCols.getClasses(th.id);

                jibits.modifyClasses(th, c);
            });
        },

        onDataChange : function ()
        {
            var rowdata = this.prov.data, n = rowdata.length, rows = new Array(n);

            for (var i = 0; i < n; ++i)
            {
                var r = createRow.call(this, rowdata, i);
                rows[i] = r;
            }

            putRows.call(this, rows);
        },

        onDragBegin : function (drag)
        {
            if (this.dragObjRows < 1)
                return null;

            var opt = { rows:this.dragObjRows, opacity:1.0, fade:this.dragObjFade,
                        width:0, nofix: {nofix:1} };
            var tbl = this.elem(), dragObj = jibits.cloneElemShallow(tbl, opt.nofix);
            var col = getDragColIndex.call(this, drag);

            dragObj.style.margin = "0";
            jibits.setOpacity(dragObj, 0.50);

            if (tbl.tHead)
                duplicateColumn(tbl.tHead, col, dragObj, opt);
            $foreach(tbl.tBodies, function (tbody)
            {
                var w = duplicateColumn(tbody, col, dragObj, opt);
                if (opt.rows <= 0)
                    $break();
            });
            if (tbl.tFoot)
                duplicateColumn(tbl.tFoot, col, dragObj, opt);

            if (opt.width)
                dragObj.style.width = opt.width + "px";

            jibits.fixClone(dragObj, { baseid : "dragGridCol"});

            return dragObj;
        },

        onDragMove : function (drag)
        {
            if (!this.dragInsertTracker)
                return;

            var which = whichColumn.call(this, drag.cx);

            if (!which)
                jibits.hide(this.dragInsertTracker);
            else
            {
                jibits.moveTo(this.dragInsertTracker, $rect(which.x,which.y,1,1));
                jibits.show(this.dragInsertTracker);
            }
        },

        onDragEnd : function (drag)
        {
            if (this.dragInsertTracker)
                jibits.hide(this.dragInsertTracker);

            var tbl = this.elem(), which = whichColumn.call(this, drag.cx);
            var from = getDragColIndex.call(this, drag);
            if (!which)
                return;

            var to = which.col - (which.col > from);
            if (from == to)
                return;

            moveColumn(this.rowTemplate.src, from, to);

            $foreach(tbl.rows, function (row)
            {
                moveColumn(row, from, to);
            });

            // Correct the visibility of the splitters and our index map:
            var self = this;
            $foreach(tbl.tHead.rows[0].cells, function (th, prevRet, index)
            {
                if (from == 0 || to == 0)
                {
                    var splitter = $elem(th.id + SPLITSFX);
                    jibits.setVisible(splitter, index != 0);
                }
                self.colIndex[th.id] = index;
                self.colIds[index] = th.id;
            });
        },

        onResizeBegin : function (drag)
        {
            var col = this.colIndex[drag.mgr.column] - 1;
            $assert(col >= 0);
            col = this.colIds[col];
            this.resizingCol = col;
            this.origColWidth = parseInt(jibits.getStyle(col, "width"));
            this.maxColWidth = this.origColWidth +
                                parseInt(jibits.getStyle(drag.mgr.column, "width"))
                                - MINCOLWIDTH;
            //console.info("col["+this.resizingCol+"].width="+this.origColWidth);
        },

        onResizeMove : function (drag)
        {
            var w = Math.max(MINCOLWIDTH, this.origColWidth + drag.dx);
            w = Math.min(w, this.maxColWidth) + "px";
            //console.info("col["+this.resizingCol+"].width="+w);
            jibits.setStyle(this.resizingCol, { width : w });
        },

        onResizeEnd : function (drag)
        {
            delete this.resizingCol;
        },

        "?zjs.browser.ie" : function ()
        {
            function adjLeftCol (sect, expr, header)
            {
                $foreach(sect.rows, function (tr)
                {
                    var c = tr.cells[0];

                    if (expr)
                        c.style.setExpression("left", expr);
                    else
                        c.style.removeExpression("left");

                    if (header)
                        return;

                    var meta = jibits.getMeta(c);
                    if (!("zIndex" in meta))
                        meta.zIndex = c.style.zIndex;

                    if (expr)
                        c.style.zIndex = jibits.getStyle(c, "zIndex") + 1;
                    else
                        c.style.zIndex = meta.zIndex;

                    c.style.position = expr ? "relative" : "";
                });
            }

            function fixTopLeft ()
            {
                var th = $elem(this.id).tHead.rows[0].cells[0];
                var meta = jibits.getMeta(th);

                if (!("zIndex" in meta))
                    meta.zIndex = th.style.zIndex;

                var z = jibits.getStyle(th, "zIndex");
                if (this.fixedLeftCol && this.fixedHeaders)
                    th.style.zIndex = z + 2;
                else if (this.fixedLeftCol || this.fixedHeaders)
                    th.style.zIndex = z + 1;
                else
                    th.style.zIndex = meta.zIndex;

                th.style.position = (this.fixedLeftCol || this.fixedHeaders)
                                            ? "relative" : "";
            }

            return (
            {
                onConnect : function ()
                {
                    $super(arguments).call(this);

                    if (this.scrollBox)
                        jibits.setStyle(this.scrollBox,
                            { overflow:"auto", zoom:1, position:"relative"});

                    if (this.fixedHeaders)
                        this.lockHeaders();
                    if (this.fixedLeftCol)
                        this.lockLeftColumn();
                },

                lockHeaders : function (lock)
                {
                    $super(arguments).apply(this, arguments);

                    var expr = this.fixedHeaders
                                    ? "document.getElementById('"+this.scrollBox+"').scrollTop-1"
                                    : null;
                    var tbl = $elem(this.id);

                    $foreach(tbl.tHead.rows[0].cells, function (th, prevRet, index)
                    {
                        if (expr)
                            th.style.setExpression("top", expr);
                        else
                            th.style.removeExpression("top");

                        if (index)
                            th.style.position = expr ? "relative" : "";
                    });

                    fixTopLeft.call(this);
                },

                lockLeftColumn : function (lock)
                {
                    $super(arguments).apply(this, arguments);

                    var expr = this.fixedLeftCol
                                    ? "document.getElementById('"+this.scrollBox+"').scrollLeft-1"
                                    : null;
                    var tbl = $elem(this.id);

                    if (tbl.tHead)
                        adjLeftCol(tbl.tHead, expr, true);
                    $foreach(tbl.tBodies, function (tbody)
                    {
                        adjLeftCol(tbody, expr);
                    });
                    if (tbl.tFoot)
                        adjLeftCol(tbl.tFoot, expr);

                    fixTopLeft.call(this);
                }
            });
        }
    });
}())

}); // namespace jibits
